/*
 * This file is part of the URI Template library.
 *
 * For licensing information please see the file license.txt included in the release.
 * A copy of this license can also be found at 
 *   http://www.opensource.org/licenses/artistic-license-2.0.php
 */
package org.weborganic.furi;

import java.util.regex.Pattern;

/**
 * A variable in a URL pattern or template.
 * 
 * The variables can be typed by prefixing the variable name. Types are not required, if no type is 
 * specified, the variable is considered untyped.
 * 
 * Note: there is no predefined list of types as the handling of types is out of scope. The syntax 
 * simply allows variables to be associated with a type.
 * 
 * Examples of variables:
 * <pre>
 *   foo         - An untyped variable named 'foo'
 *   bar         - An untyped variable named 'bar'
 *   ping:foo    - A variable named 'foo' typed 'ping'
 *   ping:foo=1  - A variable named 'foo' typed 'ping' which default value is '1'
 *   foo=pong    - An untyped variable named 'foo' which default value is 'pong'
 * </pre>
 * 
 * Variables only appear in the context of the a template expansion.
 * 
 * Expansion rule (4.4.1):
 * 
 * <pre>
 * &quot;In a variable ('var') expansion, if the variable is defined then substitute the value of 
 * the variable, otherwise substitute the default value. 
 * If no default value is given then substitute with the empty string.&quot;
 * </pre>
 * 
 * Syntax for variables:
 * <pre>
 * var         = [ vartype &quot;:&quot; ]  varname [ &quot;=&quot; vardefault ]
 * vars        = var [ *(&quot;,&quot; var) ]
 * vartype     = (ALPHA / DIGIT)* (ALPHA / DIGIT / &quot;.&quot; / &quot;_&quot; / &quot;-&quot; )
 * varname     = (ALPHA / DIGIT)* (ALPHA / DIGIT / &quot;.&quot; / &quot;_&quot; / &quot;-&quot; )
 * vardefault  = *(unreserved / pct-encoded)
 * </pre>
 * 
 * @see <a
 *      href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html">URI
 *      Template (Internet Draft 3)</a>
 * @see <a href="http://tools.ietf.org/html/rfc3986">RFC 3986 - Uniform Resource Identifier (URI):
 *      Generic Syntax<a/>
 * 
 * @author Christophe Lauret
 * @version 11 June 2009
 */
public class Variable {

  /**
   * Used for reserved variable names.
   */
  public enum Reserved {

    /**
     * The wildcard represented by the 'asterisk'
     */
    WILDCARD("*");

    /**
     * The symbol for this reserved.
     */
    private String _symbol;

    /**
     * Construct a new reserved variable - keep it private.
     * 
     * @param symbol The symbol used for this reserved variable name.
     */
    private Reserved(String symbol) {
      this._symbol = symbol;
    }

    /**
     * @return the symbol used for this reserved variable name.
     */
    String symbol() {
      return this._symbol;
    }
  };

  /**
   * Indicate that the variable's value should be processed as a list ("@") or an associative array ("%").
   * 
   * This variable type is an instruction for the template processor.
   * It is not an indication of language or implementation type.
   */
  public enum Form {

    /**
     * Indicate that this variable can be expanded as a simple string (default).
     */
    STRING,

    /**
     * Indicate that this variable can be expanded as a list of strings.
     */
    LIST,

    /**
     * Indicates that this variable can be expanded as an associated array. 
     */
    MAP;

    /**
     * Returns the type of this variable from the specified expression.
     * 
     * <p>
     * This method does not return <code>null</code>
     * 
     * @param exp The expression.
     * @return The type of this expression.
     */
    protected static Form getType(String exp) {
      if (exp.length() == 0) return STRING;
      char c = exp.charAt(0);
      if (c == '@') return LIST;
      if (c == '%') return MAP;
      return STRING;
    }

  }

  /**
   * Indicate that the variable's value should be processed as a list ("@") or an associative array ("%").
   * 
   * This variable type is an instruction for the template processor.
   * It is not an indication of language or implementation type.
   */
  public enum Modifier {

    /**
     * Indicate that this variable can be expanded as a simple string (default).
     */
    SUBSTRING,

    /**
     * Indicate that this variable can be expanded as a list of strings.
     */
    REMAINDER;

  }

  /**
   * The pattern for a valid variable name.
   */
  private static final Pattern VALID_NAME = Pattern.compile("[a-zA-Z0-9][\\w.-]*");

  /**
   * The pattern for a valid normalised variable value: any unreserved character or an escape
   * sequence. This pattern contains non-capturing parentheses to make it easier to get variable
   * values as a group.
   */
  protected static final Pattern VALID_VALUE = Pattern.compile("(?:[\\w.~-]|(?:%[0-9A-F]{2}))+");

  /**
   * The default value is an empty string.
   */
  private static final String DEFAULT_VALUE = "";

  /**
   * The type of this variable.
   */
  private Form _form = Form.STRING;

  /**
   * The implementation type of this variable (eg. string, integer, etc... can be user-defined).
   * 
   * <p>
   * Use <code>null</code> for untyped.
   */
  private VariableType _type;

  /**
   * The name of this variable.
   */
  private String _name;

  /**
   * The default value for this variable.
   */
  private String _default;

  /**
   * Creates a new untyped reserved variable.
   * 
   * @param reserved The name of the variable.
   * 
   * @throws NullPointerException If the specified name is <code>null</code>.
   * @throws IllegalArgumentException If the specified name is an empty string.
   */
  public Variable(Reserved reserved) throws NullPointerException, IllegalArgumentException {
    this._name = reserved.symbol();
    this._default = DEFAULT_VALUE;
    this._form = Form.STRING;
    this._type = null;
  }

  /**
   * Creates a new untyped variable.
   * 
   * @param name The name of the variable.
   * 
   * @throws NullPointerException If the specified name is <code>null</code>.
   * @throws IllegalArgumentException If the specified name is an empty string.
   */
  public Variable(String name) throws NullPointerException, IllegalArgumentException {
    this(name, DEFAULT_VALUE);
  }

  /**
   * Creates a new untyped variable.
   * 
   * @param name The name of the variable.
   * @param def The default value for the variable.
   * 
   * @throws NullPointerException If the specified name is <code>null</code>.
   * @throws IllegalArgumentException If the specified name is an empty string.
   */
  public Variable(String name, String def) throws NullPointerException, IllegalArgumentException {
    this(name, def, null);
  }

  /**
   * Creates a new variable.
   * 
   * @param name The name of the variable.
   * @param def  The default value for the variable.
   * @param type The type of the variable.
   * 
   * @throws NullPointerException If the specified name is <code>null</code>.
   * @throws IllegalArgumentException If the specified name is an empty string.
   */
  public Variable(String name, String def, VariableType type) throws NullPointerException,
      IllegalArgumentException {
    if (name == null)
      throw new NullPointerException("A variable must have a name, but was null");
    if (!isValidName(name))
      throw new IllegalArgumentException("The variable name is not valid: " + name);
    this._name = name;
    this._default = def != null ? def : DEFAULT_VALUE;
    this._type = type;
    this._form = Form.getType(name);
  }

  /**
   * Creates a new variable.
   * 
   * @param name The name of the variable.
   * @param def  The default value for the variable.
   * @param type The type of the variable.
   * 
   * @throws NullPointerException If the specified name is <code>null</code>.
   * @throws IllegalArgumentException If the specified name is an empty string.
   */
  public Variable(String name, String def, VariableType type, Form form) throws NullPointerException,
      IllegalArgumentException {
    if (name == null)
      throw new NullPointerException("A variable must have a name, but was null");
    if (!isValidName(name))
      throw new IllegalArgumentException("The variable name is not valid: " + name);
    this._name = name;
    this._default = def != null ? def : DEFAULT_VALUE;
    this._type = type;
    this._form = form != null? form : Form.STRING;
  }

  /**
   * Returns the form of this variable.
   * 
   * <p>
   * This method will never return <code>null</code>.
   * 
   * @return The form of this variable.
   */
  public Form form() {
    return this._form;
  }

  /**
   * Returns the name of this variable.
   * 
   * <p>
   * This method never return <code>null</code>.
   * 
   * @return The name of this variable.
   */
  public String name() {
    return this._name;
  }

  /**
   * Returns the default value for this variable.
   * 
   * This method never return <code>null</code>.
   * 
   * @return The default value for this variable.
   */
  public String defaultValue() {
    return this._default;
  }

  /**
   * Returns the implementation type of this variable.
   * 
   * <p>
   * This method will return <code>null</code> if the variable is untyped.
   * 
   * @return The type of this variable.
   */
  public VariableType type() {
    return this._type;
  }

  /**
   * Returns the expanded value of this variable.
   * 
   * If no value is specified for this variable, the default value is returned instead.
   * 
   * @param parameters The parameters.
   * 
   * @return The value.
   */
  public String value(Parameters parameters) {
    // No parameters: use the default value
    if (parameters == null)
      return this._default;
    // Defined and non-empty: return the first value in a list
    String[] values = parameters.getValues(this._name);
    if (values != null && values.length > 0 && values[0] != null) {
      return values[0];
      // Empty or undefined: return the default
    } else {
      return this._default;
    }
  }

  /**
   * Returns the expanded value of this variable.
   * 
   * If no values are specified for this variable, the default value is returned instead.
   * 
   * @param parameters The parameters.
   * 
   * @return The values.
   */
  public String[] values(Parameters parameters) {
    // No parameters: use the default value
    if (parameters == null)
      return new String[] { this._default };
    String[] values = parameters.getValues(this._name);
    // Defined and non-empty: return the values
    if (values != null && values.length > 0 && values[0].length() > 0) {
      return values;
      // Empty or undefined: return the default
    } else {
      return new String[] { this._default };
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean equals(Object o) {
    if (o == this)
      return true;
    if ((o == null) || (o.getClass() != this.getClass()))
      return false;
    Variable v = (Variable) o;
    // name and default cannot be null
    return _name.equals(v._name) && _default.equals(v._default);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int hashCode() {
    return this._name.hashCode() + 7 * this._default.hashCode();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    if (this._default.length() > 0)
      return this._name + '=' + this._default;
    else
      return this._name;
  }

  // Static helpers
  // ==============================================================================================

  /**
   * Parses the given expression and returns the corresponding instance.
   * 
   * @param exp The expression to parse.
   * 
   * @return the corresponding variable.
   * 
   * @throws URITemplateSyntaxException If the expression cannot be parsed
   */
  public static Variable parse(String exp) throws URITemplateSyntaxException {
    // Capture the form if any
    Form f = Form.getType(exp);
    if (f != Form.STRING) exp = exp.substring(1);
    int colon = exp.indexOf(':');
    // untyped
    if (colon < 0) {
      Variable v = parseUntyped(exp);
      v._form = f;
      return v;
    // ignore the empty type and treat as untyped
    } else if (colon == 0) {
      Variable v = parseUntyped(exp.substring(1));
      v._form = f;
      return v;
    // a type is specified
    } else {
      Variable v = parseUntyped(exp.substring(colon + 1));
      v._type = new VariableType(exp.substring(0, colon));
      v._form = f;
      return v;
    }
  }

  /**
   * Parses the given expression and returns the corresponding instance.
   * 
   * @param exp The expression to parse.
   * 
   * @return the corresponding variable.
   * 
   * @throws URITemplateSyntaxException If the expression cannot be parsed
   */
  private static Variable parseUntyped(String exp) throws URITemplateSyntaxException {
    int equal = exp.indexOf('=');
    if (equal == 0)
      throw new URITemplateSyntaxException(exp, "Variable name is empty string");
    if (equal > 0) {
      return new Variable(exp.substring(0, equal), exp.substring(equal + 1));
    } else {
      return new Variable(exp, null);
    }
  }

  /**
   * Indicates whether the variable has a valid name according to the specifications.
   * 
   * @param name The name of the variable.
   * 
   * @return <code>true</code> if the name is valid; <code>false</code> otherwise.
   */
  public static boolean isValidName(String name) {
    if (name == null)
      return false;
    return VALID_NAME.matcher(name).matches();
  }

  /**
   * Indicates whether the variable has a valid value according to the specifications.
   * 
   * @param value The value of the variable.
   * 
   * @return <code>true</code> if the name is not valid; <code>false</code> otherwise.
   */
  public static boolean isValidValue(String value) {
    if (value == null)
      return false;
    return VALID_VALUE.matcher(value).matches();
  }

  // helpers -------------------------------------------------------------------

  /**
   * Returns the name of this variable as a regular expression pattern string for use in a regular
   * expression.
   * 
   * <p>
   * Implementation note: this method replaces any character that could be interpreted as a regex
   * meta-character, it is more efficient than using quotation (\Q...\E) for the whole string.
   * 
   * @return The regex pattern corresponding to this name.
   */
  protected String namePatternString() {
    return this._name.indexOf('.') < 0 ? this._name : this.name().replaceAll("\\.", "\\\\.");
  }

}
